/*
 * Copyright (c) 2011 Obix Labs Limited
 * Redistribution and use in source and binary forms, 
 * with or without modification, are permitted provided 
 * that the following conditions are met:
 * 
 * 		Redistribution of source code must retain the above 
 * 		copyright notice, this list of conditions and the 
 * 		following disclaimer.
 *
 * 		Redistribution in binary form must reproduce the 
 * 		above copyright notice, this list of conditions 
 * 		and the following disclaimer in the documentation 
 * 		and/or other materials provided with the distribution.
 * 
 * 	THIS SOFTWARE IS PROVIDED "AS IS," WITHOUT A WARRANTY OF 
 * 	ANY KIND. ALL EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS 
 * 	AND WARRANTIES, INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, 
 * 	FITNESS FOR A PARTICULAR PURPOSE OR NON-INFRINGEMENT, 
 * 	ARE HEREBY EXCLUDED. OBIX LABS LIMITED ("Obix Labs") AND ITS 
 * 	LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE 
 * 	AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR 
 * 	ITS DERIVATIVES. IN NO EVENT WILL Obix Labs OR ITS LICENSORS BE 
 * 	LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR DIRECT, 
 * 	INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE 
 * 	DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF 
 * 	LIABILITY, ARISING OUT OF THE USE OF OR INABILITY TO USE THIS 
 * 	SOFTWARE, EVEN IF Obix Labs HAS BEEN ADVISED OF THE POSSIBILITY OF 
 * 	SUCH DAMAGES.
 */
 
package com.obixlabs.commons.xml;

import java.io.StringReader;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.validation.Schema;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.xml.sax.InputSource;

import com.obixlabs.commons.ObixException;
import com.obixlabs.commons.io.IOResourceCloser;


/**
 * <p>
 * A utility class providing XML operations.
 * </p>
 */
public class XMLUtilities 
{
	private static final Logger logger = 
    	LoggerFactory.getLogger(XMLUtilities.class.getName());        

	/**
	 * <p>
	 * Parses the specified raw-data into an XML document. 
	 * This method is intended primarily for legacy documents 
	 * still governed by a DTD. For more modern documents 
	 * governed by an XML-Schema, please use the 
	 * {@link #parseDocument(byte[], Schema)} method instead.
	 * </p>
	 *  
	 * @param xmlData	The XML to be parsed, in byte/raw form.
	 * @param validate	Indicates that a validating XML parser
	 * should be used for parsing the document. If this 
	 * parameter is set to <code>true</code>, the DTD must be 
	 * specified in the document and must be accessible.
	 *  
	 * @return	A document instance corresponding to the specified 
	 * XML byte sequence.
	 * @throws ObixException	If an error occurs parsing or validating 
	 * the document.
	 */
	public static Document parseDocument(	byte[] xmlData, 
											boolean validate)
											throws ObixException
	{
		Document result;

		String xmlString = toXmlString(xmlData);
		StringReader xmlStringReader = null;

		try
		{
			ObixXmlValidationErrorHandler defaultDocumentHandler = 
								new ObixXmlValidationErrorHandler();

			// create and initialise the document builder
			DocumentBuilderFactory documentBuilderFactory = 
				DocumentBuilderFactory.newInstance();

			xmlStringReader = new StringReader(xmlString);
			InputSource xmlnputSource = 
					new InputSource(xmlStringReader);

			DocumentBuilder documentBuilder;
			if (validate)			
				documentBuilderFactory.setValidating(true);
			else documentBuilderFactory.setValidating(false);
			
			documentBuilder = 
				documentBuilderFactory.newDocumentBuilder();
			documentBuilder.setErrorHandler(defaultDocumentHandler);
			
			
			// now parse the document
			result = documentBuilder.parse(xmlnputSource);

			// if validate was false, then this object would 
			// be empty by implication
			if (defaultDocumentHandler.warningsOccured())
			{
				String errorText = 
						"Warnings occured validating XML document. '"
						+ defaultDocumentHandler.formatWarnings();
				logger.warn(errorText);
			}

			if (defaultDocumentHandler.errorsOccured())
			{
				String errorText = "Errors occured validating XML document. '"
						+ defaultDocumentHandler.formatErrors();
				throw new ObixException(errorText);
			}

		}
		catch (Exception exce)
		{ throw ObixException.wrapException(exce); }
		finally
		{ IOResourceCloser.close(xmlStringReader); }

		return result;
	}	
	
	
	/**
	 * <p>
	 * Parses the specified data into an XML document. This method also 
	 * allows the caller to specify a schema against which the XML-data 
	 * should be validated. If no schema is supplied, then no validation 
	 * is performed.
	 * </p>
	 * 
	 * @param xmlData	The raw bytes which to parse into an XML document.
	 * @param schema	The schema against which the specified data should 
	 * be validated. Note that this parameter can be null, in which case, no
	 * validation will be performed on the XML data.
	 * @return	A document instance encapsulating the parsed 
	 * XML data.
	 * @throws ObixException	If an error occurs parsing or validating 
	 * the document.
	 */
	public static Document parseDocument(	byte[] xmlData,
											Schema schema)
											throws ObixException
	{
		Document result;
		
        String xmlString =toXmlString(xmlData);
        StringReader xmlStringReader = null;
                        
        try
        {
    		ObixXmlValidationErrorHandler defaultDocumentHandler
    			= new ObixXmlValidationErrorHandler();
    		
            // create and initialise the document builder
            DocumentBuilderFactory documentBuilderFactory = 
                    DocumentBuilderFactory.newInstance();                        
                                    
            xmlStringReader = new StringReader(xmlString);
            InputSource xmlnputSource = 
            		new InputSource(xmlStringReader);

            DocumentBuilder documentBuilder; 
            if (schema!=null)
            {
            	documentBuilderFactory.setValidating(false);
                documentBuilderFactory.setNamespaceAware(true);
                documentBuilderFactory.setSchema(schema);
                                	
                documentBuilder = 
                	documentBuilderFactory.newDocumentBuilder();
                documentBuilder.setErrorHandler(defaultDocumentHandler);
            }
            else documentBuilder = 
            		documentBuilderFactory.newDocumentBuilder();

            
            //now parse the document
            result = documentBuilder.parse(xmlnputSource);

            //if schema was null, then this object would be empty 
            //anyway, as it would not have been associated
            //to the document builder
            if (defaultDocumentHandler.warningsOccured())
            {
            	String errorText = 
            		"Warnings occured validating XML document. '"
                    + defaultDocumentHandler.formatWarnings();
            	logger.warn(errorText);
            }

            if (defaultDocumentHandler.errorsOccured())
            {
            	String errorText = 
            		"Errors occured validating XML document. '"
                    + defaultDocumentHandler.formatErrors();
            	throw new ObixException(errorText);
            }

        }
		catch (Exception exce) 
		{ throw ObixException.wrapException(exce); }
        finally 
        { IOResourceCloser.close(xmlStringReader); }		
		
		return result;
	}
	
    
    /**
	 * <p>
	 * Convenience method which extracts the value of 
	 * an attribute from an XML node.
	 * </p>
	 * 
	 * @param node	The node in which the attribute occurs,
	 * and from which the attribute value is to be extracted.
	 * 
	 * @param attributeName	The name/identifier of the attribute.
	 * 
	 * @return	The value of the attribute as taken from 
	 * the given node. 
	 */
	public static  String getAttributeValueFromNode(Node node, 
													String attributeName)
	{
		String result;
		Node attribute = node.getAttributes().getNamedItem(attributeName);
		result = attribute.getNodeValue();
		return result;		
	}


	/**
     * <p>
     * Removes any non-word characters to the front of the XML data 
     * array so as to reduce possibility of unexpected SAXParserExceptions. 
     * </p> 
     * 
     * @param xmlData	The XML data array from which to strip non-word 
     * characters.
     * @return	A String which corresponds to the XML argument with all 
     * non-word characters removed from the front.
     */  
    private static String toXmlString(byte[] xmlData)
    {
    	String result = new String (xmlData);
    	result = result.trim().replaceFirst("^([\\W]+)<","<");
    	return result;
    }
}//end class def